/*
 * Copyright (C) 2013 Brett Wooldridge
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.zaxxer.nuprocess.osx;

import com.sun.jna.Memory;
import com.sun.jna.Native;
import com.sun.jna.Pointer;
import com.sun.jna.StringArray;
import com.sun.jna.ptr.IntByReference;
import com.zaxxer.nuprocess.NuProcess;
import com.zaxxer.nuprocess.NuProcessHandler;
import com.zaxxer.nuprocess.internal.BasePosixProcess;
import com.zaxxer.nuprocess.internal.IEventProcessor;
import com.zaxxer.nuprocess.internal.LibC;
import com.zaxxer.nuprocess.internal.LibC.SyscallLibrary;

import java.nio.file.Path;
import java.util.List;
import java.util.logging.Level;

/**
 * @author Brett Wooldridge
 */
class OsxProcess extends BasePosixProcess {
    static {
        for (int i = 0; i < processors.length; i++) {
            processors[i] = new ProcessKqueue();
        }

        // Setup a private signal for waking up the kqueue processing threads
        LibC.signal(LibC.SIGUSR2, LibC.SIG_IGN);
    }

    OsxProcess(NuProcessHandler processListener) {
        super(processListener);
    }

    @Override
    public NuProcess start(List<String> command, String[] environment, Path cwd) {
        callPreStart();

        String[] commands = command.toArray(new String[0]);

        Pointer posix_spawn_file_actions = createPosixPipes();
        Pointer posix_spawnattr = createPosixSpawnAttributes();

        try {
            int rc = prepareProcess(environment, cwd, commands, posix_spawn_file_actions, posix_spawnattr);
            checkReturnCode(rc, "Invocation of posix_spawn() failed");

            afterStart();

            // On macOS, registration can be immediately followed by queueing read handling for stdout
            // and stderr without risking a racy exit because processes are launched suspended and are
            // only resumed after NuProcessHandler.onStart is called
            registerProcess();
            myProcessor.queueRead(this);

            callStart();

            singleProcessContinue();
        } catch (RuntimeException e) {
            // TODO remove from event processor pid map?
            LOGGER.log(Level.WARNING, "Exception thrown from handler", e);
            onExit(Integer.MIN_VALUE);
            return null;
        } finally {
            LibC.posix_spawnattr_destroy(posix_spawnattr);
            LibC.posix_spawn_file_actions_destroy(posix_spawn_file_actions);

            // After we've spawned, close the unused ends of our pipes (that were dup'd into the child process space)
            closePipes();
        }

        return this;
    }

    @Override
    public void run(List<String> command, String[] environment, Path cwd) {
        callPreStart();

        String[] commands = command.toArray(new String[0]);

        Pointer posix_spawn_file_actions = createPosixPipes();
        Pointer posix_spawnattr = createPosixSpawnAttributes();

        try {
            int rc = prepareProcess(environment, cwd, commands, posix_spawn_file_actions, posix_spawnattr);
            checkReturnCode(rc, "Invocation of posix_spawn() failed");

            afterStart();

            myProcessor = (IEventProcessor) new ProcessKqueue(this);

            callStart();

            singleProcessContinue();

            myProcessor.run();
        } catch (RuntimeException e) {
            // TODO remove from event processor pid map?
            LOGGER.log(Level.WARNING, "Exception thrown from handler", e);
            onExit(Integer.MIN_VALUE);
        } finally {
            LibC.posix_spawnattr_destroy(posix_spawnattr);
            LibC.posix_spawn_file_actions_destroy(posix_spawn_file_actions);

            // After we've spawned, close the unused ends of our pipes (that were dup'd into the child process space)
            closePipes();
        }
    }

    private int prepareProcess(String[] environment, Path cwd, String[] commands,
                               Pointer posix_spawn_file_actions, Pointer posix_spawnattr) {
        int rc = LibC.posix_spawnattr_init(posix_spawnattr);
        checkReturnCode(rc, "Internal call to posix_spawnattr_init() failed");

        LibC.posix_spawnattr_setflags(posix_spawnattr, (short) (LibC.POSIX_SPAWN_START_SUSPENDED | LibC.POSIX_SPAWN_CLOEXEC_DEFAULT));

        IntByReference restrict_pid = new IntByReference();
        StringArray commandsArray = new StringArray(commands);
        StringArray environmentArray = new StringArray(environment);
        if (cwd != null) {
            rc = spawnWithCwd(restrict_pid, commands[0], posix_spawn_file_actions, posix_spawnattr, commandsArray, environmentArray, cwd);
        } else {
            rc = LibC.posix_spawnp(restrict_pid, commands[0], posix_spawn_file_actions, posix_spawnattr, commandsArray, environmentArray);
        }

        pid = restrict_pid.getValue();

        initializeBuffers();

        return rc;
    }

    private void closePipes() {
        LibC.close(stdinWidow);
        LibC.close(stdoutWidow);
        LibC.close(stderrWidow);
    }

    private int spawnWithCwd(final IntByReference restrict_pid,
                             final String restrict_path,
                             final Pointer file_actions,
                             final Pointer /*const posix_spawnattr_t*/ restrict_attrp,
                             final StringArray /*String[]*/ argv,
                             final Pointer /*String[]*/ envp,
                             final Path cwd) {
        int cwdBufSize = 1024;
        long peer = Native.malloc(cwdBufSize);
        Pointer oldCwd = new Pointer(peer);
        LibC.getcwd(oldCwd, cwdBufSize);
        String newCwd = cwd.toAbsolutePath().toString();
        int rc = LibC.SYSCALL.syscall(SyscallLibrary.SYS___pthread_chdir, newCwd);
        checkReturnCode(rc, "syscall(SYS__pthread_chdir) failed to set current directory");

        try {
            return LibC.posix_spawnp(restrict_pid, restrict_path, file_actions, restrict_attrp, argv, envp);
        } finally {
            rc = LibC.SYSCALL.syscall(SyscallLibrary.SYS___pthread_chdir, oldCwd);
            Native.free(Pointer.nativeValue(oldCwd));
            checkReturnCode(rc, "syscall(SYS__pthread_chdir) failed to restore current directory");
        }
    }

    private void singleProcessContinue() {
        // Signal the spawned process to continue (unsuspend)
        LibC.kill(pid, LibC.SIGCONT);
    }

    private Pointer createPosixPipes() {
        int rc;

        Pointer posix_spawn_file_actions = createPosixSpawnFileActions();

        try {
            int[] fds = createPipes();

            // Create spawn file actions
            rc = LibC.posix_spawn_file_actions_init(posix_spawn_file_actions);
            checkReturnCode(rc, "Internal call to posix_spawn_file_actions_init() failed");

            // Dup the reading end of the pipe into the sub-process, and close our end
            rc = LibC.posix_spawn_file_actions_adddup2(posix_spawn_file_actions, stdinWidow, 0);
            checkReturnCode(rc, "Internal call to posix_spawn_file_actions_adddup2() failed");

            rc = LibC.posix_spawn_file_actions_addclose(posix_spawn_file_actions, fds[0]);
            checkReturnCode(rc, "Internal call to posix_spawn_file_actions_addclose() failed");

            // Dup the writing end of the pipe into the sub-process, and close our end
            rc = LibC.posix_spawn_file_actions_adddup2(posix_spawn_file_actions, stdoutWidow, 1);
            checkReturnCode(rc, "Internal call to posix_spawn_file_actions_adddup2() failed");

            rc = LibC.posix_spawn_file_actions_addclose(posix_spawn_file_actions, fds[1]);
            checkReturnCode(rc, "Internal call to posix_spawn_file_actions_addclose() failed");

            // Dup the writing end of the pipe into the sub-process, and close our end
            rc = LibC.posix_spawn_file_actions_adddup2(posix_spawn_file_actions, stderrWidow, 2);
            checkReturnCode(rc, "Internal call to posix_spawn_file_actions_adddup2() failed");

            rc = LibC.posix_spawn_file_actions_addclose(posix_spawn_file_actions, fds[2]);
            checkReturnCode(rc, "Internal call to posix_spawn_file_actions_addclose() failed");

            return posix_spawn_file_actions;
        } catch (RuntimeException e) {
            LOGGER.log(Level.WARNING, "Exception creating posix pipe actions", e);
            LibC.posix_spawn_file_actions_destroy(posix_spawn_file_actions);
            throw e;
        }
    }

    private Pointer createPosixSpawnFileActions() {
        return new Memory(Native.POINTER_SIZE);
    }

    private Pointer createPosixSpawnAttributes() {
        return new Memory(Native.POINTER_SIZE);
    }
}
